﻿/*
 * Copyright 2005-2011 Edward L. Stauff.                  Contact: <EdStauff@gmail.com>.
 * 
 * This file, "TimeSpanParser.cs", is part of the "Stauffware.Common.Time" class library.
 * "Stauffware.Common.Time" is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser Public License as published by the 
 * Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common.Time" is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * the "Stauffware.Common.Time" sources.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Globalization;
using Stauffware.Common.Core;

namespace Stauffware.Common.Time.TimespanParsingUnderDevelopment
{
	/// <summary>
	/// Options for parsing timespans.
	/// </summary>
	[Flags]
	public enum TimeSpanParseOptions : long
	{
		/// <summary>
		/// No options.
		/// </summary>
		None = 0,

		#region ParseOptions compatibility

		/// <summary>
		/// Allow leading whitespace.  (Equal to ParseOptions.AllowLeadingWhite.)
		/// </summary>
		AllowLeadingWhite	= ParseOptions.AllowLeadingWhite,

		/// <summary>
		/// Allow trailing whitespace.  (Equal to ParseOptions.AllowTrailingWhite.)
		/// </summary>
		AllowTrailingWhite	= ParseOptions.AllowTrailingWhite,

		/// <summary>
		/// Allow inner whitespace (but never between digits or time separator).
		/// (Equal to ParseOptions.AllowInnerWhite.)
		/// </summary>
		AllowInnerWhite		= ParseOptions.AllowInnerWhite,

		/// <summary>
		/// Equivalent to combining AllowLeadingWhite, AllowInnerWhite, and AllowTrailingWhite.
		/// (Equal to ParseOptions.AllowWhiteSpace.)
		/// </summary>
		AllowWhiteSpace		= ParseOptions.AllowWhiteSpace,

		/// <summary>
		/// Forces parsing of the entire input string.
		/// Without this option, extraneous characters following a valid time will
		/// terminate the parsing, but not cause an error.
		/// (Equal to ParseOptions.ParseEntireInput.)
		/// </summary>
		ParseEntireInput	= ParseOptions.ParseEntireInput,

		#endregion
	}

	/// <summary>
	/// Represents one or more units of time.
	/// </summary>
	[Flags()]
	public enum TimeUnits
	{
		/// <summary></summary>
		Millisecond = 0x01,
		/// <summary></summary>
		Seconds = 0x02,
		/// <summary></summary>
		Minutes = 0x04,
		/// <summary></summary>
		Hours = 0x08,
		/// <summary></summary>
		Days = 0x10,
		/// <summary></summary>
		Weeks = 0x20,
		/// <summary></summary>
		Years = 0x80,
		/// <summary></summary>
		None = 0x00,
		/// <summary></summary>
		Time = Hours | Minutes | Seconds,
		/// <summary></summary>
		All = 0xFF
	}

	//#################################################################################
	/// Extension methods

	public static class TimeSpanParseOptions_Extensions
	{
		/// <summary>
		/// Converts a ParseOptions value to a TimeParseOptions value.
		/// </summary>
		/// <param name="o"></param>
		/// <returns></returns>
		public static TimeSpanParseOptions ToTimeParseOptions (this ParseOptions o)
		{
			return (TimeSpanParseOptions) o;
		}
	}

	//#################################################################################
	/// <summary>
	/// A parser for parsing a time span from a string into a TimeSpan.
	/// </summary>
	/// <remarks>
	/// This implementation is currently under development (note the namespace!) and
	/// is not functional yet.
	/// 
	/// Syntax:
	///		timespan	:=	["+" | "-"] unsignedTS
	///		unsignedTS	:=	"0" | [days] time | float unit
	///		days		:=	int ("d" | "da" | "day" | "days")
	///		time		:=	digit [digit] ":" digit [digit] ":" digit [digit] ["." digit...]
	///		int			:=	{ unsigned integer }
	///		float		:=	{ unsigned floating-point number }
	///		unit		:=	{ any valid time unit name or abbreviation, or an unambiguous prefix thereof }
	/// </remarks>

	public class TimeSpanParser
	{
		/// <summary>
		/// What to use when there isn't a TimeSpanParseOptions parameter.
		/// </summary>
		public const TimeSpanParseOptions DefaultParseOptions = 
			TimeSpanParseOptions.AllowWhiteSpace | TimeSpanParseOptions.ParseEntireInput;

		internal readonly TimeSpanParseOptions Options;

		internal readonly PrefixDictionary<TimeUnits> UnitNames = new PrefixDictionary<TimeUnits>(8, true);

		//-----------------------------------------------------------------------------
		// class constructor

		static TimeSpanParser ()
		{


		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructs a parser that uses DefaultParseOptions.
		/// </summary>
		public TimeSpanParser ( ) : this(DefaultParseOptions, null) { }

		/// <summary>
		/// Constructs a parser.
		/// </summary>
		/// <param name="options">parsing options</param>
		public TimeSpanParser (TimeSpanParseOptions options) : this(options, null) { }

		/// <summary>
		/// Constructs a parser that uses DefaultParseOptions.
		/// </summary>
		/// <param name="ifp">formatting info, or null</param>
		public TimeSpanParser (IFormatProvider ifp) : this(DefaultParseOptions, ifp) { }

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Constructs a parser.
		/// </summary>
		/// <param name="options">parsing options</param>
		/// <param name="ifp">format provider</param>

		public TimeSpanParser (TimeSpanParseOptions options, IFormatProvider ifp)
		{
			this.Options = options;
			
			// TEMPORARILY HARD-CODED
			this.UnitNames.Add("days", TimeUnits.Days);
			this.UnitNames.Add("hours", TimeUnits.Hours);
			this.UnitNames.Add("hrs", TimeUnits.Hours);
			this.UnitNames.Add("milliseconds", TimeUnits.Millisecond);
			this.UnitNames.Add("minutes", TimeUnits.Minutes);
			this.UnitNames.Add("ms", TimeUnits.Millisecond);
			this.UnitNames.Add("seconds", TimeUnits.Seconds);
			this.UnitNames.Add("weeks", TimeUnits.Weeks);
			this.UnitNames.Add("wks", TimeUnits.Weeks);
			this.UnitNames.Add("years", TimeUnits.Years);
			this.UnitNames.Add("yrs", TimeUnits.Years);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Parses a time span.  
		/// Should never throw an exception except for failed assertions (programming errors).
		/// </summary>
		/// <param name="input">the string to be parsed</param>
		/// <returns>
		///		The result of the parse attempt, whether successful or not; will never return null.
		/// </returns>

		public TimeSpanParse Parse (string input)
		{
			string originalInput = input;

			TimeSpanParse parse = new TimeSpanParse();

			parse._foundUnits = TimeUnits.None;

			// TO-DO: commas could be handled more carefully
			input = input.Replace(",", "").ToLower().Trim();

			if (input == "")
				return parse;

			parse._value = TimeSpan.Zero;

			// look for time components separated by colons

			int colon = input.IndexOf(':');
			if (colon >= 0)
			{
				int space = input.LastIndexOf(' ', 0, colon);
				if (space < 0) space = 0;
				string timeStr = input.Substring(space).Trim();
				input = input.Substring(0, space).Trim();

				parse._value = TimeSpan.Parse(input);
				parse._foundUnits = TimeUnits.Hours | TimeUnits.Minutes | TimeUnits.Seconds;
			}

			// look for components specified by unit name

			while (input != "")
			{
				// look for a number						BOGUS PARSING!!!
				int n = 0;
				while (n < input.Length && char.IsDigit(input[n]))
					n++;
				if (n < input.Length && input[n] == '.')
				{
					n++;
					while (n < input.Length && char.IsDigit(input[n]))
						n++;
				}

				if (n == 0)
				{
					parse._errorMessage = TimeResources.MissingNumberInTimeSpan;
				//	parse._parsedText = 
					return parse;
				}

				double number = double.Parse(input.Substring(0, n));
				input = input.Substring(n).TrimStart();

				// check for solitary zero
				if (input == "" && number == 0 && parse._foundUnits == TimeUnits.None)
				{
					parse._value = TimeSpan.Zero;
					parse._parsedText = originalInput;
					return parse;
				}

				// look for a unit name

				for (n = 0; n < input.Length; n++)
					if (!char.IsLetter(input[n]))
						break;

				if (n == 0)
				{
				//	parse._parsedText = 
					parse._errorMessage = TimeResources.MissingUnitInTimeSpan;
					return parse;
				}

#if false	// COMPILE ERRORS

				string name = input.Substring(0, n);
				input = input.Substring(n).Trim();
				TimeUnits unit = LexUnitName(name);

				if (unit == TimeUnits.None)
				{
					parse._errorMessage = TimeResources.InvalidUnitInTimeSpan.Replace("{UNIT}", name);
					return parse;
				}

				if (!unit.HasSingleUnit())
				{
					parse._errorMessage = TimeResources.AmbiguousUnitInTimeSpan.Replace("{UNIT}", name);
					return parse;
				}

				if ((parse._foundUnits & unit) != 0)
				{
					parse._errorMessage = TimeResources.RepeatedUnitInTimeSpan.Replace("{UNIT}", unit.ToString());
					return parse;
				}

				parse._foundUnits = parse._foundUnits | unit;

				parse._value += CreateTimeSpan(number, unit);
#endif
			}

			return parse;
		}

		//-----------------------------------------------------------------------------

		private enum TokenType
		{
			EndOfInput,
			Unit,
			Int,
			Float,
			Colon,
			Bogus
		}

		//-----------------------------------------------------------------------------
	}

	//#################################################################################
	/// <summary>
	/// Encapsulates the result of trying to parse a time span from a string into a TimeSpan,
	/// whether successful or unsuccessful.
	/// </summary>

	public class TimeSpanParse : ValueParse<TimeSpan>
	{
		/// <summary>
		/// Gets the units that were found in the parsed string.
		/// </summary>
		public TimeUnits FoundUnits { get { return this._foundUnits; } }
		internal TimeUnits _foundUnits;
	}

	//#################################################################################
}
